#include "vtkFieldImport.hpp"
#include "bc_base_all.hpp"
#include "functionParserWrapper.hpp"

namespace hms::vtk
{

template<typename SolverType>
void readZeroGradient(
	const BoundaryPatch& patch,
	SolverBase<SolverType>& solver,
	typename SolverBase<SolverType>::FieldName fieldName
){
	using MeshType = typename SolverBase<SolverType>::MeshType;
	Field<MeshType>& f { solver.field(fieldName) };
	f.setBoundaryCondition( ZeroGradient{f, patch} );
	// solver.field(fieldName)
	// 	.template setBoundaryCondition<ZeroGradient<MeshType>>(patch);
}

template<typename SolverType>
void readDynamicValue(
	const xml::XMLElement* bcElem,
	const BoundaryPatch& patch,
	SolverBase<SolverType>& solver,
	typename SolverBase<SolverType>::FieldName fieldName
){
	using MeshType = typename SolverBase<SolverType>::MeshType;

	bool
		isUniform { getAttributeValueWithDefault(bcElem, "isUniform", false) },
		isFunction {getAttributeValueWithDefault(bcElem, "isFunction",false) };

	Field<MeshType>& field { solver.field(fieldName) };
	using DV = DynamicValue<MeshType>;
	DV& dv { field.template setBoundaryCondition<DV>( patch, solver.time() ) };

	if ( isFunction ){
		std::vector<FunctionParserWrapper> parsers { readFunctions(
			bcElem, field.nComponents(), true
		) };
		dv.functions( std::move(parsers), isUniform );
		dv.zField( solver.zField() );
	} else {
		Index nCols { isUniform ? 1 : patch.size() };
		dv.values(
			readTimeSeries(bcElem, field.nComponents(), nCols),
			isUniform
		);
	}
}

template<typename SolverType>
void readStaticValue(
	const xml::XMLElement* bcElem,
	const BoundaryPatch& patch,
	SolverBase<SolverType>& solver,
	typename SolverBase<SolverType>::FieldName fieldName
){
	using MeshType = typename SolverBase<SolverType>::MeshType;

	auto fieldNameStr = [&](){
		return std::string{ magic_enum::enum_name(fieldName) };
	};

	bool
		isUniform { getAttributeValueWithDefault(bcElem, "isUniform", false) },
		isFunction {getAttributeValueWithDefault(bcElem, "isFunction",false) };

	Field<MeshType>& field { solver.field(fieldName) };

	using SV = StaticValue<MeshType>;
	SV& sv { field.template setBoundaryCondition<SV>(patch, isUniform) };

	ArrayXXs vals ( field.nComponents(), isUniform ? 1 : patch.size() );
	if ( !isFunction ){
		textToMatrix( bcElem, vals );
	} else {
		std::vector<FunctionParserWrapper> parsers { readFunctions(
			bcElem, field.nComponents(), false
		) };
		if ( isUniform ){
			solver.logger().warn(
				"Field \"{}\", patch \"{}\": Evaluating static value boundary "
				"as uniform function:\n"
				"{} ({}) are {}!\n{}",
				fieldNameStr(),
				patch.name(),
				bold("All variables"),
				FunctionParserWrapper::allVars(),
				bold("set to zero"),
				nodeString(bcElem)
			);
			for ( int i{0}; i<field.nComponents(); ++i ){
				vals(i,0) = parsers[i].eval();
			}
		} else { // non-uniform function
			setPatchByFunction( field, patch, solver.zField(), solver.time(), parsers );
		}
	}
	/* if it's a non-uniform function, the values have already been set through
	 * setPatchByFunction. Otherwise, set them now. */
	if ( !isFunction || isUniform ){
		sv.set(vals, isUniform);
	}
}


template<typename SolverType>
boundaryCondition::Type readBoundaryConditionType(
	const xml::XMLElement* bcElem,
	const std::string& patchName,
	SolverBase<SolverType>& solver,
	typename SolverBase<SolverType>::FieldName fieldName
){
	using T = boundaryCondition::Type;

	auto fieldNameStr = [&](){
		return std::string{ magic_enum::enum_name(fieldName) };
	};

	const char* bcTypeStr { bcElem->Attribute("Type") };

	if ( !bcTypeStr )
		throw ParseError(
			"Could not find attribute \"type\" for boundary condition of field \""
			+ fieldNameStr() + "\", patch \"" + patchName + "\" in\n"
			+ nodeString(bcElem)
		);

	std::optional<T> bcType { magic_enum::enum_cast<T>(bcTypeStr) };
	if ( !bcType.has_value() ){
		/* try solver-specific boundary condition types */
		bcType = stringToBoundaryType( solver, fieldName, bcTypeStr );
		/* if it still doesn't hold a type, throw error */
		if ( !bcType.has_value() )
			throw ParseError(
				"Boundary condition type \""s + bcTypeStr + "\" unknown!\n"
				+ nodeString(bcElem)
				+ "\nAvailable boundary condition types are: "
				+ availableBoundaryTypes()
			);
	}
	return bcType.value();
}


template<typename SolverType>
void readBoundaryCondition(
	const xml::XMLElement* patchElem,
	const BoundaryPatch& patch,
	SolverBase<SolverType>& solver,
	typename SolverBase<SolverType>::FieldName fieldName
){
	using MeshType = typename SolverBase<SolverType>::MeshType;
	using T = boundaryCondition::Type;

	std::string fieldNameStr { magic_enum::enum_name(fieldName) };

	const xml::XMLElement* bcElem { getChildNodeByAttributeValue(
		patchElem, "BoundaryCondition", "Field", fieldNameStr.c_str()
	) };

	T bcType { readBoundaryConditionType( bcElem, patch.name(), solver, fieldName)};

	switch(bcType){
		case T::zeroGradient:
			readZeroGradient(patch, solver, fieldName);
			break;
		case T::staticValue:
			readStaticValue(bcElem, patch, solver, fieldName);
			break;
		case T::dynamicValue:
			readDynamicValue(bcElem, patch, solver, fieldName);
			break;
		default:
			if ( std::is_same_v<SolverType, SWESolver<MeshType>> )
				readSWEBoundaryCondition(
					bcElem, patch, solver, fieldName, bcType
				);
	}
}


template<typename SolverType>
void readBoundaryConditions(
	const xml::XMLDocument* doc, SolverBase<SolverType>& solver, bool skipZField
){
	using S = SolverBase<SolverType>;
	using FieldName = typename S::FieldName;
	constexpr static FieldName zFieldName { S::zFieldName() };

	/* analogously to export, we first set up a lookup table for the relation
	 * boundary patch -> patch node */
	const xml::XMLElement* patchesNode { getBoundaryNode(doc) };

	const std::vector<BoundaryPatch>& patches { solver.mesh().boundaryPatches() };
	std::vector<const xml::XMLElement*> patchNodes ( patches.size() );

	for ( size_t i{0}; i < patches.size(); ++i ){
		patchNodes[i] = getBoundaryPatch( patchesNode, patches[i] );
	}

	auto readBoundary = [&]( FieldName f ){
		for ( size_t i{0}; i < patches.size(); ++i ){
			readBoundaryCondition(patchNodes[i], patches[i], solver, f);
		}
	};

	/* First, we set the z field, so that functions can use the z values.
	 * However, in case the z field is read in via DEM reader or similar,
	 * we don't want to overwrite it */
	if ( !skipZField )
		readBoundary( zFieldName );

	/* now, we set the other fields */
	for ( size_t i {0}; i < S::nFields; ++i ){
		FieldName fieldName { static_cast<FieldName>(i) };
		/* z field has already been set */
		if ( fieldName != zFieldName ){
			readBoundary( fieldName );
		}
	}
}


} // namespace hms::vtk
